Skip to content

fix(worktree): recover empty cleanup shells - #650

Merged
xintaofei merged 11 commits into
xintaofei:mainfrom
dawNotPoi:fix/642-clean-worktree-cleanup
Sep 4, 2026
Merged

fix(worktree): recover empty cleanup shells#650
xintaofei merged 11 commits into
xintaofei:mainfrom
dawNotPoi:fix/642-clean-worktree-cleanup

Conversation

@dawNotPoi

Copy link
Copy Markdown
Contributor

Summary

  • recover cleanup when Git has already detached a worktree but left an empty directory shell behind
  • remove only a proven-empty shell before pruning stale registration and deleting the task branch
  • preserve fail-closed behavior for non-empty or unreadable paths while leaving normal worktree removal unchanged

Testing

  • cargo test --no-default-features --features test-utils --lib worktree_ -- --nocapture (41 passed)
  • cargo check --no-default-features --features test-utils --lib
  • cargo clippy --no-default-features --features test-utils --lib --tests -- -D warnings
  • git diff --check

Repository-wide cargo fmt --check still reports pre-existing formatting differences outside this change; the modified hunks were checked separately.

Closes #642

@dawNotPoi
dawNotPoi marked this pull request as ready for review September 4, 2026 07:18
`path_holds_uncommitted` measured a post-removal shell with `git status`
run from the shell itself, and git walks up from there. A worktree root
configured inside the project — a relative `worktree_root` — puts the
shell under the project's own `.git`, so git answered about the PROJECT:
a project with a stray file of its own made an empty shell read as
"holds uncommitted files" and the cleanup retry never converged. Decide
from the directory alone whenever the `.git` marker is gone, since a
path git no longer speaks for cannot be described by its answer.

Report the filesystem half of `remove_worktree_and_branch` through a
message that names the path and the OS reason: `AppCommandError`'s
`Display` is its `message` alone, so `AppCommandError::io` left bare
"I/O operation failed" on the task card.

Cover the probe directly, in the DEFAULT worktree layout (beside the
project, outside every repository) — the existing fixture nests its
worktree inside the repo, where `git status` always answers and the
empty-shell branch is never reached.
…topped

Resolve the removal's filesystem probes from the project repository, the
way git resolves the same argument: `std::fs` would have used the app's
own working directory, so a project sitting at a filesystem root — where
`sibling_path` has nothing to prefix and returns a bare name — could
have `remove_dir` reach for a namesake beside the app while git still
held the real checkout.

Keep the OS error code on the filesystem failures instead of flattening
every one to `IoError`, and correct where that message is read: it lands
in the task's `cleanup_failed` timeline event, not on the card.

Make the two probe tests measure a modified TRACKED file. `has_changes`
spawns git through the inherited environment, so an untracked fixture is
only as visible as the developer's global ignore rules allow.
The join was justified with a project sitting at a filesystem root, but
that project cannot have a worktree at all: `basename` is empty there, so
the generated name starts with a dash and `git worktree add` reads it as
options. State what the join actually buys — the removal and its own
probe agreeing on one directory — and say plainly that it does not make a
relative folder path coherent, because the engine's gates still read that
path from the app's working directory.

Map `AlreadyExists` too, which POSIX allows `rmdir` to return for a
non-empty directory, so replacing `AppCommandError::io` costs only its
message. Stop the probe fixture from claiming a guarantee it cannot give
about repositories above the system temp directory.
…icts git

The pre-check existed to keep a marker-less directory away from `git
worktree remove`, but git needs no such help: it validates the `.git`
marker before deleting anything and refuses outright, contents untouched
— measured across all four shapes (registered or not, empty or not).
Asking git first is therefore free, and the fallback it already had
recovers every leftover the pre-check did.

What made that safe to collapse is `remove_dir` being non-recursive: it
can only ever succeed on an empty directory, so a shell holding a file
fails closed whichever call reaches it.

The one thing the pre-check did carry is which failure to report, so that
moves to where it belongs. A checkout git still speaks for keeps git's
reporting; a detached shell has none worth keeping — git's only word about
it names a `.git` the user never heard of, while the removal that failed
was ours. Both directions are pinned, the second by a locked worktree.
Git looks a `<worktree>` argument up by unique path suffix BEFORE it
resolves it as a path, so a relative name is not scoped to the repository
it is handed to. With a checkout registered at
`/tmp/elsewhere/repo-task-7`, `git worktree remove --force repo-task-7`
run from an unrelated repository deletes that one, uncommitted files
included — measured, and now pinned by a test that loses `precious.txt`
without this change. Folder paths are stored exactly as they were given,
so the argument was only ever as absolute as whoever made the folder.

Resolving it here also collapses the last gap between the two halves of
this function: git and `std::fs` now act on one directory that neither
can reinterpret.
Two inputs could still send git somewhere the filesystem half was not
looking. A Windows drive-relative path makes `join` drop `repo_path`
entirely, leaving `C:trees` for a per-drive current directory the app and
git do not share. And a path the OS holds as non-UTF-8 came out of
`to_string_lossy` as a DIFFERENT absolute path, which git would have gone
and deleted while every removal here still used the original bytes.

Both now stop the removal rather than guess at it. Handing git a
directory other than the one we probed is the single thing this
resolution exists to prevent, so it cannot be the thing it falls back to.
The previous guard asked whether `join` had dropped its base, which a
drive-relative PROJECT path answers wrongly: `C:repo` joined with
`C:repo\..\victim` still starts with `C:repo`, so the check passed a path
whose meaning the process and the git child would each finish from their
own per-drive current directory.

Ask the question that was always meant instead — is either side relative
to a drive rather than rooted on one — and refuse whichever side carries
it. Unix has no prefixes, so the predicate is false for every path there;
what a Unix run pins is that no shape a real folder row holds is mistaken
for the Windows form.
@xintaofei

Copy link
Copy Markdown
Owner

codeg work task 153 is done — #650 (2 files, +420/-46).

Windows CI caught both of these, and the first one caught it fairly: the
predicate test asserted `C:repo` was ordinary "because unix has no path
prefixes", which is true of the machine it was written on and not of the
one the guard exists for. Windows read the drive prefix and refused the
path, exactly as designed — so the assertion now states that outcome per
platform, and the branch I could not exercise locally is covered.

The second was a test measuring the platform rather than the code: `git
worktree list` prints forward slashes on Windows while the fixture path
holds backslashes, and a temp directory can come back short-named, so the
string never matched even though nothing had been pruned. Ask git inside
the checkout instead — a swept worktree cannot answer for its own HEAD —
which compares nothing and says the same thing.

Both are test-only. The removal behavior they cover was already correct
on Windows: everything asserted before the failing lines passed there.
@xintaofei
xintaofei merged commit b4b6865 into xintaofei:main Sep 4, 2026
7 checks passed
xintaofei added a commit that referenced this pull request Sep 5, 2026
Talking to an agent while it is still replying now works on every session that has a delivery channel — with attachments, shown as a real message in the transcript, and recoverable if the agent finished without reading it.
Mobile gets a terminal key bar and a ⋯ menu on every file-tree row.

## New

- **Send a message into a running turn on any session with a delivery channel** — Codex, Grok, Gemini and OpenCode sessions get the mid-turn send too, worded honestly per channel: Claude inserts into the current turn, the others leave a note the agent picks up at its next check, shown as waiting until it actually does. (#637, @Adam-Dalloul)
- **A message sent mid-turn now reads as a message** — it lands in the transcript as its own turn, and the reply to it starts a new turn instead of running into the previous answer mid-paragraph. (#636, @Adam-Dalloul)
- **Mid-turn sends carry images and file references**, instead of going dark the moment you stage an attachment. An image shows on the message right away in the running turn rather than only after reopening the conversation, and if the turn ends first the whole draft — attachment included — is queued for the next one rather than dropped. (#640, @Adam-Dalloul)
- **A note the agent finished without reading is no longer lost** — the list stays up after the turn, says the agent never got to it, and offers to send it as an ordinary message or dismiss it.
- **"Adapter version" is now a per-agent setting** — follow the newest adapter release instead of waiting for the reviewed pin, which is what makes a brand-new model reachable the day it ships. Pinned stays the default and recommended; the newest release is unreviewed and can break the agent, and an install that can't fetch it falls back to the pinned version rather than failing. (#635, @Adam-Dalloul)
- **A virtual key bar for the terminal on mobile** — ESC, TAB, CTRL, ALT, arrows, Home/End and PgUp/PgDn, with CTRL/ALT latches that wrap the next key you type, lifted clear of the soft keyboard and collapsible from the tab bar. (#647, @mrytsr)
- **Every file-tree row gets a ⋯ button** that opens the same menu right-click does, so touch devices have a way in now that long-press is reserved for drag. (#648, @mrytsr)
- **Transcript file links open beside the conversation when the workspace column is off screen** — on the task board and the infinite canvas, a file badge, a markdown link or "view diff" now opens a read-only viewer next to the transcript, with "Open in workspace" one click away, instead of doing nothing visible.
- **Codex reports its background terminals** (codex-acp 1.10.0) — a long-running command Codex pushes to the background is badged "Background" and joins the live task strip with its Stop button and output link, the way Claude's already did.
- **"Disable hardware acceleration" now works on Linux**, not just Windows.

## Improved

- **Updated bundled agents:** Kimi Code 0.41.0, CodeBuddy 2.144.0, Qoder 1.1.44, Cursor 2026.09.02, Antigravity 1.1.1, OpenCode 1.18.29, plus the Claude and Codex adapters (claude-agent-acp 0.74.0, codex-acp 1.10.0).
- **Antigravity now takes a custom version** in Agent Settings, like the other downloaded agents — and a version that was never published now fails the download instead of quietly filing the pinned build under the number you asked for.
- **Signing out of Claude mid-session now ends only that turn** — the session stays open and asks to be signed in again, instead of the connection dropping and the conversation flipping to cancelled. A delegated sub-agent in the same state reports "needs sign-in" rather than an unrecognized failure.

## Fixed

- **A Codex search that finds nothing now says "No matches"** instead of a red failed tool call. (#651, reported by @rubybrowncoat; fixed by @dawNotPoi in #652)
- **Cleaning up a task with no changes now removes its worktree completely**, instead of half-removing it and then reporting it still holds uncommitted files so the retry never finished. (#642, reported by @rubybrowncoat; fixed by @dawNotPoi in #650)
- **Worktree removal now acts on exactly the checkout you pointed at.** Git matches a worktree argument by path suffix before resolving it as a path, so a same-named checkout belonging to another project could be deleted, uncommitted files included; a path that resolves two ways is now refused rather than guessed at.
- **A `/goal` turn no longer appears twice** — a slash command is now matched against what codeg actually sent, so its turn stops coming back as a background overlay beside itself. (#643, @Adam-Dalloul)
- **"Fork from here" no longer silently forks the whole reply** when you point at the half before a mid-turn message — the button greys out with an explanation until the fork point is named, the way it already does during a running turn.
- **Turning "Disable hardware acceleration" back off now takes effect** after the restart, instead of the setting being inherited by the new process and staying on.

Thanks to @Adam-Dalloul, @mrytsr and @dawNotPoi for contributing to this release, and to @rubybrowncoat for the reports.

-----------------------------

# 发布版本 0.30.3

智能体正在回复时插话,现在在每一个有投递通道的会话上都能用了——能带图片和文件、在会话记录里就是一条正经消息,智能体没来得及读完就收尾的,也还能捞回来。
移动端还多了终端虚拟按键条,文件树每一行也多了 ⋯ 菜单。

## 新增

- **只要会话有投递通道,就能往正在跑的这一轮里发消息**——Codex、Grok、Gemini、OpenCode 也都有了,而且措辞按通道如实区分:Claude 是直接插进当前轮次,其余的是留一张便条、等智能体下次查看时取走,在它真的读到之前一直显示为等待中。(#637@Adam-Dalloul)
- **中途发出的消息现在就是一条消息**——它会作为独立的一轮出现在会话记录里,针对它的回答也会另起一轮,不再和上一段答案粘在同一个气泡里连成一片。(#636@Adam-Dalloul)
- **中途发送支持图片和文件引用**,不再一贴附件按钮就变灰。图片当场就显示在正在进行的这一轮里,不用关掉会话再打开才看得到;万一那一轮刚好结束了,整条草稿(连同附件)会排进下一轮,而不是被丢掉。(#640@Adam-Dalloul)
- **智能体没读就收尾的便条不会再消失**——列表在这一轮结束后仍然保留,写明智能体没来得及读,并提供「作为消息发送」或直接忽略。
- **「适配器版本」成为每个智能体各自的设置项**——可以选择跟随最新的适配器发布,不必等官方钉住的版本更新,新模型上线当天就能用上。默认仍是钉住的版本并标为推荐;最新版未经审核、可能让智能体不可用,安装时若取不到最新版会自动回退到钉住的版本,而不是直接装不上。(#635@Adam-Dalloul)
- **移动端终端虚拟按键条**——ESC、TAB、CTRL、ALT、方向键、Home/End、PgUp/PgDn,CTRL/ALT 按下后会锁定并作用于你输入的下一个字符,按键条会浮在软键盘上方,也可以从标签栏收起。(#647@mrytsr)
- **文件树每一行都多了 ⋯ 按钮**,点开就是右键那份菜单——长按手势已留给拖拽,触屏这下有了入口。(#648@mrytsr)
- **工作区被挡住时,会话里的文件链接改在旁边打开**——在任务看板和无限画布这类整页视图下,点文件徽章、Markdown 链接或「查看差异」会在会话旁弹出只读预览(「在工作区中打开」也只有一步之遥),不再点了没反应。
- **Codex 的后台终端能报上来了**(随 codex-acp 1.10.0 到位)——被 Codex 挂到后台的长命令会标上「Background」,并进入顶部的实时任务条,带停止按钮和输出链接,和 Claude 的一样。
- **「禁用硬件加速」在 Linux 上也生效了**,不再只支持 Windows。

## 改进

- **内置智能体版本更新:** Kimi Code 0.41.0、CodeBuddy 2.144.0、Qoder 1.1.44、Cursor 2026.09.02、Antigravity 1.1.1、OpenCode 1.18.29,以及 Claude 和 Codex 的适配器(claude-agent-acp 0.74.0、codex-acp 1.10.0)。
- **Antigravity 现在也能指定自定义版本了**,和其他下载安装的智能体一样;填了一个从未发布过的版本号会直接下载失败,而不是把钉住的那份安装包悄悄记成你填的号。
- **会话进行中登出 Claude 现在只结束当前这一轮**——会话本身保留,只是提示需要重新登录,不再整条连接断开、会话被标成已取消。委托出去的子智能体遇到同样情况会报「需要登录」,而不是一句看不懂的异常结束。

## 修复

- **Codex 搜索没有命中时显示「无匹配结果」**,不再是一张红色的失败工具卡。(#651,由 @rubybrowncoat 反馈;@dawNotPoi#652 中修复)
- **清理没有改动的任务时,工作树现在能被完整删除**,不再删一半、然后反过来说它还有未提交的改动,导致重试永远走不完。(#642,由 @rubybrowncoat 反馈;@dawNotPoi#650 中修复)
- **删除工作树只会作用于你指定的那一个。** git 会先按路径后缀去匹配工作树参数、再当作路径解析,因此另一个项目下同名的 checkout 有可能被连同未提交文件一起删掉;现在遇到会有两种解释的路径会直接拒绝,而不是替你猜。
- **`/goal` 之类的斜杠命令不再出现两遍**——斜杠命令现在会和 codeg 实际发出的内容比对,那一轮不会再以「后台活动」的形式在自己旁边重复一次。(#643@Adam-Dalloul)
- **「从这里分叉」不再悄悄把整条回复都分出去**——当你点的是中途插话之前的那半段时,按钮会像回复生成中那样变灰并说明原因,直到分叉点被确定。
- **「禁用硬件加速」关掉后重启真的会生效**,不再被新进程继承下来、看起来像是没关掉。

感谢 @Adam-Dalloul@mrytsr@dawNotPoi 为本次发布做出的贡献,也感谢 @rubybrowncoat 的问题反馈。
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Clean zero-change task cleanup partially removes worktree, then falsely reports uncommitted files

2 participants